﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using WCell.Constants;
using WCell.Constants.World;
using WCell.MPQTool;
using WCell.Tools.Maps.Parsing.ADT;
using WCell.Util.Graphics;
using WCell.Util.Toolshed;

namespace WCell.Tools.Maps
{
    public static class WMOExtractor
    {
        private static MpqManager manager;
        private const string baseDir = "WORLD\\maps\\";
        private const string outputDir = "D:\\Work\\Collision\\WMOs";
        private const string fileType = "wmo";
        private static List<uint> LoadedWMOIds;
        private const float RadiansPerDegree = 0.0174532925f;

        #region Write
        [Tool]
        public static void WriteWMOFiles()
        {
            var wowRootDir = DBCTool.FindWowDir(null);
            manager = new MpqManager(wowRootDir);

            var entryList = DBCMapReader.GetMapEntries();
            if (entryList == null)
            {
                Console.WriteLine("Error retrieving MapEntries.");
                return;
            }

            foreach (var mapEntry in entryList)
            {
                //if (mapEntry.Id != (int)MapId.Kalimdor) continue;

                var dir = mapEntry.MapDirName;
                var wdtDir = Path.Combine(baseDir, dir);
                var wdtName = dir;

                var wdt = WDTParser.Process(manager, wdtDir, wdtName);
                if (wdt == null) continue;
                
                var regionBuildings = ExtractRegionWMOs(wdt);
                
                var max = regionBuildings.HasTiles ? TerrainConstants.TilesPerMapSide : 1;
                for (var tileY = 0; tileY < max; tileY++)
                {
                    for (var tileX = 0; tileX < max; tileX++)
                    {
                        //if (tileX != 39) continue;
                        //if (tileY != 27) continue;

                        if (regionBuildings.ObjectsByTile[tileX, tileY] == null) continue;

                        // Don't bother writing tiles with no buildings.
                        var tileBuildings = regionBuildings.ObjectsByTile[tileX, tileY];
                        if (tileBuildings.Buildings.Count == 0) continue;

                        var path = Path.Combine(outputDir, mapEntry.Id.ToString());
                        if (!Directory.Exists(path))
                            Directory.CreateDirectory(path);
                        var fileName = String.Format("{0:00}{1:00}.fub", tileX, tileY);
                        var file = File.Create(Path.Combine(path, fileName));

                        WriteTileBuildings(file, tileBuildings);
                        
                        file.Close();
                    }
                }
                
            }

            Console.WriteLine("Done.");
        }

        private static void WriteTileBuildings(Stream file, TileBuildings tileBuildings)
        {
            if (tileBuildings == null)
            {
                Console.WriteLine("Cannot write null TileBuildings to file.");
                return;
            }

            // The magic fileType
            var writer = new BinaryWriter(file);
            writer.Write(fileType);

            // Number of Buildings we're storing
            writer.Write(tileBuildings.Buildings.Count);
            foreach (var building in tileBuildings.Buildings)
            {
                WriteBuilding(writer, building);
            }
        }

        private static void WriteBuilding(BinaryWriter writer, Building building)
        {
            if (building == null)
            {
                Console.WriteLine("Cannot write null Building to file.");
                return;
            }

            //BoundingBox, store it
            writer.Write(building.Bounds);
            
            // Rotation Degrees, store them
            writer.Write(building.RotationModelY);

            // WorldPos, store it
            writer.Write(building.WorldPos);

            // Write the number of Groups
            writer.Write(building.BuildingGroups.Length);
            for (var i = 0; i < building.BuildingGroups.Length; i++)
            {
                WriteBuildingGroup(writer, building.BuildingGroups[i]);
            }
        }

        private static void WriteBuildingGroup(BinaryWriter writer, BuildingGroup buildingGroup)
        {
            if (buildingGroup == null)
            {
                Console.WriteLine("Cannot write null BuildingGroup to file.");
                return;
            }

            // Pack and store the Group's Bounds
            writer.Write(buildingGroup.Bounds);
            
            writer.Write(buildingGroup.Vertices.Length);
            for (var i = 0; i < buildingGroup.Vertices.Length; i++)
            {
                writer.Write(buildingGroup.Vertices[i]);
            }

            WriteBSPTree(writer, buildingGroup.Tree);
        }

        private static void WriteBSPTree(BinaryWriter writer, BSPTree tree)
        {
            writer.Write(tree.rootId);
            
            writer.Write(tree.nodes.Length);
            for (var i = 0; i < tree.nodes.Length; i++)
            {
                WriteBSPNode(writer, tree.nodes[i]);
            }
        }
        
        private static void WriteBSPNode(BinaryWriter writer, BSPNode node)
        {
            writer.Write((byte)node.flags);
            writer.Write(node.negChild);
            writer.Write(node.posChild);
            writer.Write(node.planeDist);

            if (node.TriIndices != null)
            {
                writer.Write(node.TriIndices.Length);
                for (var i = 0; i < node.TriIndices.Length; i++)
                {
                    writer.Write(node.TriIndices[i]);
                }
            }
            else
            {
                // Consider a null TriIndex array to have 0 length.
                writer.Write(0);
            }
        }
        #endregion

        #region Read
        [Tool]
        public static void ViewRegionBuildings()
        {
            var regionBuildings = ReadRegionBuildings(MapId.EasternKingdoms);
            var buildings = regionBuildings.ObjectsByTile[36, 49].Buildings;

            var highestVec = new Vector3(float.MinValue, float.MinValue, float.MinValue);
            Building highestBuilding = null;
            foreach (var building in buildings)
            {
                var groups = building.BuildingGroups;
                foreach (var buildingGroup in groups)
                {
                    var verts = buildingGroup.Vertices;
                    foreach (var vector3 in verts)
                    {
                        if (vector3.Y < highestVec.Y) continue;
                        highestVec = vector3;
                        highestBuilding = building;
                    }
                }
            }

            Vector3 newVec;
            var worldVec = highestVec;
            if (highestBuilding != null)
            {
                var center = (highestBuilding.Bounds.Min + highestBuilding.Bounds.Max)/2.0f;
                newVec = Vector3.Transform(highestVec, Matrix.CreateRotationY((highestBuilding.RotationModelY - 90)*RadiansPerDegree));

                // shift newVec to world space
                worldVec = new Vector3(newVec.Z, newVec.X, newVec.Y);
                worldVec += center;
            }

            Console.WriteLine(String.Format("X: {0}, Y: {1}, Z: {2}", worldVec.X, worldVec.Y, worldVec.Z));
        }

        public static RegionBuildings ReadRegionBuildings(MapId id)
        {
            var filePath = Path.Combine(outputDir, ((int)id).ToString());
            if (!Directory.Exists(filePath))
            {
                throw new DirectoryNotFoundException(filePath);
            }

            var regionBuildings = new RegionBuildings();

            var count = 0;
            var max = TerrainConstants.TilesPerMapSide;
            for (var tileY = 0; tileY < max; tileY++)
            {
                for (var tileX = 0; tileX < max; tileX++)
                {
                    if (tileX != 36) continue;
                    if (tileY != 49) continue;

                    var fullPath = Path.Combine(filePath, String.Format("{0:00}{1:00}.fub", tileX, tileY));
                    if (!File.Exists(fullPath)) continue;

                    var file = File.OpenRead(fullPath);
                    var reader = new BinaryReader(file);

                    var tileBuildings = ReadTileBuildings(reader);

                    regionBuildings.ObjectsByTile[tileX, tileY] = tileBuildings;
                    count++;

                    file.Close();
                }
            }
            if (count > 1) regionBuildings.HasTiles = true;

            return regionBuildings;
        }

        public static TileBuildings ReadTileBuildings(BinaryReader reader)
        {
            var magix = reader.ReadString();
            if (magix != fileType)
            {
                Console.WriteLine("Invalid file format, suckah!");
                return null;
            }

            Building[] buildings;
            var numBuildings = reader.ReadUInt16();
            if (numBuildings > 0)
            {
                buildings = new Building[numBuildings];
                for (var i = 0; i < numBuildings; i++)
                {
                    buildings[i] = ReadBuilding(reader);
                }
            }
            else
            {
                buildings = new Building[0];
            }

            return new TileBuildings {
                Buildings = new List<Building>(buildings)
            };
        }

        public static Building ReadBuilding(BinaryReader reader)
        {
            var building = new Building {
                Bounds = reader.ReadPackedBoundingBox(),
                RotationModelY = reader.ReadPackedFloat(),
                WorldPos = reader.ReadPackedVector3()
            };

            BuildingGroup[] groups;
            var numGroups = reader.ReadUInt16();
            if (numGroups > 0)
            {
                groups = new BuildingGroup[numGroups];
                for (var i = 0; i < numGroups; i++)
                {
                    groups[i] = ReadBuildingGroup(reader);
                }
            }
            else
            {
                groups = null;
            }
            building.BuildingGroups = groups;

            return building;
        }

        public static BuildingGroup ReadBuildingGroup(BinaryReader reader)
        {
            var group = new BuildingGroup {
                Bounds = reader.ReadPackedBoundingBox()
            };

            Vector3[] vertices;
            var numVerts = reader.ReadUInt16();
            if (numVerts > 0)
            {
                vertices = new Vector3[numVerts];
                for (var i = 0; i < numVerts; i++)
                {
                    vertices[i] = reader.ReadPackedVector3();
                }
            }
            else
            {
                vertices = null;
            }
            group.Vertices = vertices;

            group.Tree = ReadBSPTree(reader);
            return group;
        }

        public static BSPTree ReadBSPTree(BinaryReader reader)
        {
            var rootId = reader.ReadInt16();
            BSPNode[] nodes;

            var numNodes = reader.ReadUInt16();
            if (numNodes > 0)
            {
                nodes = new BSPNode[numNodes];
                for (var i = 0; i < numNodes; i++)
                {
                    nodes[i] = ReadBSPNode(reader);
                }
            }
            else
            {
                nodes = null;
            }

            return new BSPTree(rootId, nodes);
        }

        public static BSPNode ReadBSPNode(BinaryReader reader)
        {
            var node = new BSPNode {
                flags = ((BSPNodeFlags)reader.ReadByte()),
                negChild = reader.ReadInt16(),
                posChild = reader.ReadInt16(),
                planeDist = reader.ReadPackedFloat()
            };

            var numIndices = reader.ReadUInt16();
            if (numIndices > 0)
            {
                var indices = new Index3[numIndices];
                for (var i = 0; i < numIndices; i++)
                {
                    indices[i] = reader.ReadIndex3();
                }
                node.TriIndices = indices;
            }
            else
            {
                node.TriIndices = null;
            }

            return node;
        }
        #endregion

        #region Extract
        public static RegionBuildings ExtractRegionWMOs(WDTFile wdt)
        {
            LoadedWMOIds = new List<uint>(250);
            RegionBuildings buildings;

            if (wdt == null) return null;
            if ((wdt.Header.Header1 & WDTFlags.GlobalWMO) != 0)
            {
                // No terrain, load the global WMO
                if (wdt.ObjectDefinitions == null) return null;

                buildings = new RegionBuildings {
                    HasTiles = false,
                    ObjectsByTile = new TileBuildings[1,1]
                };

                buildings.ObjectsByTile[0, 0] = ExtractWDTWMOs(wdt.ObjectDefinitions);
                
                LoadedWMOIds.Clear();
                return buildings;
            }


            buildings = new RegionBuildings {
                HasTiles = true
            };

            for (var tileY = 0; tileY < TerrainConstants.TilesPerMapSide; tileY++ )
            {
                for (var tileX = 0; tileX < TerrainConstants.TilesPerMapSide; tileX++)
                {
                    //if (tileX != 39) continue;
                    //if (tileY != 27) continue;

                    if (!wdt.TileProfile[tileX, tileY]) continue;
                    var adtName = string.Format("{0}_{1:00}_{2:00}", wdt.Name, tileX, tileY);
                    var adt = ADTParser.Process(manager, wdt.Path, adtName);
                    if (adt == null) continue;

                    buildings.ObjectsByTile[tileX, tileY] = ExtractTileWMOs(tileX, tileY, adt.ObjectDefinitions);
                }
            }

            LoadedWMOIds.Clear();
            return buildings;
        }

        public static TileBuildings ExtractWDTWMOs(List<MapObjectDefinition> wmos)
        {
            var buildings = new TileBuildings {
                Buildings = new List<Building>(wmos.Count)
            };

            for(var i = 0; i < wmos.Count; i++)
            {
                var wmo = wmos[i];
                if (LoadedWMOIds.Contains(wmo.UniqueId)) continue;

                var filePath = wmo.FileName;
                var building = ExtractWMO(filePath);

                // Correct the World Positioning
                var calcExtents = GetWMOBounds(wmo, building.BuildingGroups);
                var worldExtents = ToWorldCoords(calcExtents);

                building.Bounds = worldExtents;
                building.RotationModelY = wmo.OrientationB;
                var worldPos = new Vector3(TerrainConstants.CenterPoint - wmo.Position.X,
                                           wmo.Position.Y,
                                           TerrainConstants.CenterPoint - wmo.Position.Z);
                building.WorldPos = ToWorldCoords(ref worldPos);

                buildings.Buildings.Add(building);

                LoadedWMOIds.AddUnique(wmo.UniqueId);
            }

            return buildings;
        }

        public static TileBuildings ExtractTileWMOs(int tileX, int tileY, List<MapObjectDefinition> wmos)
        {
            var buildings = new TileBuildings {
                Buildings = new List<Building>(wmos.Count)
            };

            for(var i = 0; i < wmos.Count; i++)
            {
                var wmo = wmos[i];
                if (LoadedWMOIds.Contains(wmo.UniqueId)) continue;
                var worldPos = new Vector3(TerrainConstants.CenterPoint - wmo.Position.X,
                                           wmo.Position.Y,
                                           TerrainConstants.CenterPoint - wmo.Position.Z);
                worldPos = ToWorldCoords(ref worldPos);

                // If this WMO belongs to another tile, skip it.
                var wmoTileX = (int)((TerrainConstants.CenterPoint - worldPos.Y) / TerrainConstants.TileSize);
                var wmoTileY = (int)((TerrainConstants.CenterPoint - worldPos.X) / TerrainConstants.TileSize);
                if (wmoTileX != tileX || wmoTileY != tileY) continue;

                var filePath = wmo.FileName;
                var building = ExtractWMO(filePath);

                // Correct the World Positioning
                var calcExtents = GetWMOBounds(wmo, building.BuildingGroups);
                var worldExtents = ToWorldCoords(calcExtents);

                building.Bounds = worldExtents;
                building.RotationModelY = wmo.OrientationB;
                building.WorldPos = worldPos;

                buildings.Buildings.Add(building);

                LoadedWMOIds.AddUnique(wmo.UniqueId);
            }

            return buildings;
        }

        public static Building ExtractWMO(string path)
        {
            var root = WMORootParser.Process(manager, path);

            if (root == null)
            {
                Console.WriteLine("Invalid WMORoot returned.");
                return null;
            }

            var building = new Building {
                BuildingGroups = new BuildingGroup[root.Header.GroupCount]
            };

            building.BuildingGroups = ExtractGroups(path, root.Header.GroupCount, root);


            return building;
        }

        public static BuildingGroup[] ExtractGroups(string path, uint count, WMORoot root)
        {
            var groups = new BuildingGroup[count];
            for (var wmoGroup = 0; wmoGroup < count; wmoGroup++)
            {
                var newFile = path.Substring(0, path.LastIndexOf('.'));
                var filePath = String.Format("{0}_{1:000}.wmo", newFile, wmoGroup);

                var group = WMOGroupParser.Process(manager, filePath, root, wmoGroup);

                var bounds = GetGroupBounds(group.Verticies);

                var newGroup = new BuildingGroup {
                    Bounds = bounds,
                    Vertices = group.Verticies.ToArray(),
                    Tree = new BSPTree(group.BSPNodes)
                };

                groups[wmoGroup] = newGroup;
            }
            return groups;
        }

        private static BoundingBox ToWorldCoords(BoundingBox boundingBox)
        {
            var newMin = ToWorldCoords(ref boundingBox.Min);
            var newMax = ToWorldCoords(ref boundingBox.Max);
            return new BoundingBox(Vector3.Min(newMin, newMax), Vector3.Max(newMin, newMax));
        }

        private static Vector3 ToWorldCoords(ref Vector3 vec)
        {
            var newX = vec.Z;
            var newY = vec.X;
            var newZ = vec.Y;

            return new Vector3(newX, newY, newZ);
        }

        private static BoundingBox GetWMOBounds(MapObjectDefinition wmoDef, IEnumerable<BuildingGroup> groups)
        {
            if (groups == null) return BoundingBox.INVALID;

            var boxen = new List<BoundingBox>();
            foreach (var buildingGroup in groups)
            {
                boxen.Add(buildingGroup.Bounds);
            }
            var newBox = GetOuterBounds(boxen.ToArray());
            
            // Translate the world position
            var worldCenter = new Vector3(TerrainConstants.CenterPoint - wmoDef.Position.X,
                                     wmoDef.Position.Y,
                                     TerrainConstants.CenterPoint - wmoDef.Position.Z);
            
            // Rotate to the model's final position
            var rotateY = Matrix.CreateRotationY((wmoDef.OrientationB - 90) * RadiansPerDegree);
            var rotMin = Vector3.Transform(newBox.Min, rotateY) + worldCenter;
            var rotMax = Vector3.Transform(newBox.Max, rotateY) + worldCenter;

            // Make a new axis-aligned bounding box
            var newMin = Vector3.Min(rotMin, rotMax);
            var newMax = Vector3.Max(rotMin, rotMax);

            return new BoundingBox(newMin, newMax);
        }

        private static BoundingBox GetOuterBounds(BoundingBox[] boxen)
        {
            if (boxen == null)
                return BoundingBox.INVALID;
            if (boxen.Length < 1)
                return BoundingBox.INVALID;
            if (boxen.Length < 2)
                return boxen[0];

            var newBox = BoundingBox.Join(ref boxen[0], ref boxen[1]);
            for (var i = 2; i < boxen.Length; i++)
            {
                newBox = BoundingBox.Join(ref newBox, ref boxen[i]);
            }
            return newBox;
        }

        private static BoundingBox GetGroupBounds(IList<Vector3> vertices)
        {
            var min = new Vector3(float.MaxValue);
            var max = new Vector3(float.MinValue);

            for (var i = 0; i < vertices.Count; i++)
            {
                var vertex = vertices[i];
                min = Vector3.Min(vertex, min);
                max = Vector3.Max(vertex, max);
            }

            return new BoundingBox(min, max);
        }
        #endregion
    }

    public class RegionBuildings
    {
        public bool HasTiles = true;
        public TileBuildings[,] ObjectsByTile = new TileBuildings[TerrainConstants.TilesPerMapSide, TerrainConstants.TilesPerMapSide];
    }

    public class TileBuildings
    {
        public List<Building> Buildings;
    }

    public class Building
    {
        public BoundingBox Bounds;
        public float RotationModelY;
        public Vector3 WorldPos;
        public BuildingGroup[] BuildingGroups;
    }

    public class BuildingGroup
    {
        public BoundingBox Bounds;
        public Vector3[] Vertices;
        public BSPTree Tree;
    }
}
